Skip to content

feat: add runtime in-page channel events - #358

Merged
antfu merged 15 commits into
devframes:mainfrom
posva:feat/in-page-channel-events
Sep 8, 2026
Merged

feat: add runtime in-page channel events#358
antfu merged 15 commits into
devframes:mainfrom
posva:feat/in-page-channel-events

Conversation

@posva

@posva posva commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

I realized that I need to listen to events dynamically, so the current setup doesn't work. I also noticed that callEvent could use a better name like emit()

  • channel.emit() + channel.on(); on() returns an unsubscribe function.
  • Allow declared type: 'event' functions to omit handler
  • Keep callEvent() as a deprecated compatibility alias.

This makes me think that channel.events might also be a bit confusing right now, I did try to go for this initially

Another thought: this implementation infer events from the types directly: functions that return void. This is not correct as an action can return void and just be used to catch errors or timeouts. I think this PR is big enough to leave that to another one but here are 2 possibilities:

  • Adding a different events property (and maybe reworking the shape of InPageChannelProtocol to also have functions):

    interface InPageChannelProtocol {
      functions: {
        /** Functions implemented by the page script. */
        pageScript?: Record<string, (...args: any[]) => any>
        /** Functions received by panels. */
        panel?: Record<string, (...args: any[]) => any>
      },
    
      events: {
        /**
         * Events emitted by the panel and listened on by the page script.
         */
        pageScript?: Record<string, (...args: any[]) => any>
        /**
         * Events emitted by the page script and listened on by the panel.
         */
        panel?: Record<string, (...args: any[]) => any>
    
      }
    
      /**
       * Shared-state slots. The page script is the authority: it owns the
       * canonical value; panels are seeded on connect and converge through
       * syncId-deduplicated patches.
       */
      sharedStates?: Record<string, object>
    }
  • A type marker:

    pageScript: {
      // event if void
      save: (value: string) => void
      // explicit action that returns void
      reset: InPageAction<() => void>
    }

Copilot AI lite review requested due to automatic review settings September 4, 2026 15:31
@coldtea-pr-lens

coldtea-pr-lens Bot commented Sep 4, 2026

Copy link
Copy Markdown

◈ PR Lens

🟢 +1 new · 🟠 ~4 changed · 🔴 -0 removed · 2 flows · 7 files · commit f37858e


Architecture

Architecture diagram for devframes/devframe at f37858e

5 components touched across 2 lanes.

Open the interactive canvas


Inside the changed components — 1 view

Component view — In-page channel bridge

Internal endpoint wiring, function registry, and diagnostics for in-page events.

Architecture view of Component view — In-page channel bridge in devframes/devframe

Data flow

Data flow diagram for devframes/devframe at f37858e

Emitting and handling in-page events · Subscribing to channel events

Open the interactive canvas


The other flows — 1 sequence

Subscribing to channel events

Sequence diagram of Subscribing to channel events in devframes/devframe

Drill down
Client Runtimes & UI — 4 components
🟡 CHANGED In-Page Channel Bridge

Browser-side communication bridge connecting page scripts and devframe panels.

🟡 CHANGED Channel Endpoints

Page-script and panel endpoints providing emit and on methods.

🟡 CHANGED Function Registry

Maintains function definitions, registers runtime event listeners, and dispatches calls.

🟢 NEW Channel Diagnostics

Defines DF0077 diagnostic error for unregistered event subscriptions.

Built-in Devframes — 1 component
🟡 CHANGED A11y Inspector Devframe

Accessibility inspector panel updated to use emit for page communication.


View

  • Architecture lens
  • Data flow lens
  • Expand every detail
  • Show unchanged neighbours

Tip

Draw a diff before it is even a pull request: npx @coldtea/pr-lens-cli analyze --base origin/main reads the diff with your own model key, and npx @coldtea/pr-lens-cli render .pr-lens/graph.json draws the same lenses on your machine.

🪧 More tips
  • Run PR Lens on your own machine: npx skills add coldteadotai/pr-lens installs the agent skill. Then tell your coding agent: "Diagram the change you just made with PR Lens and attach it to the pull request."
  • The boxes under View are live. Tick Architecture lens or Data flow lens to choose which diagrams appear, or Expand every detail to open every drill-down at once. The comment redraws in place a few seconds later.
  • Show unchanged neighbours lists the components this change did not touch alongside the ones it did, so the drill-down shows what the changed code sits next to.
  • GitHub will not let you zoom an image in a comment. The link under each diagram opens it on an interactive canvas, where you can zoom, pan and step through the flow.
  • The CLI's render picks up .github/pr-lens.yml automatically and applies your corrections (renames, exclusions, lane pins) at draw time.
  • Would you rather run it from CI on a key of your own? Add .github/workflows/pr-lens.yml with coldteadotai/pr-lens/packages/action@v0 and a model key in your repository secrets, say GEMINI_API_KEY. The Action asks Gemini by default, or OpenAI and any endpoint speaking /chat/completions through its provider input.
  • PR Lens is free for open source. A star on the repository is what keeps it going.
  • Push a new commit and the whole comment re-renders for the new head. An older run never overwrites a newer one, so a slow render cannot put a stale diagram back.
  • The diagrams follow your GitHub theme, so dark mode gets the dark render and light mode the light one, and the moving dots show this pull request's data in motion.

◈ Rendered by PR Lens · crafted with ❤️ by the Coldtea team · Come say hi on Discord

@vercel

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown

@posva is attempting to deploy a commit to the NuxtLabs Team on Vercel.

A member of the Team first needs to authorize it.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

defineChannelFunction() can unintentionally allow missing handlers when type is omitted due to generic inference, weakening type safety for the helper API.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds runtime event subscription and a clearer emission API to the in-page channel so panels/page scripts can dynamically subscribe/unsubscribe to in-page events, while keeping compatibility with the existing callEvent() API.

Changes:

  • Introduces typed channel.emit() plus runtime channel.on() (returning an unsubscribe function) on both page-script and panel endpoints; keeps callEvent() as a deprecated alias.
  • Allows type: 'event' function declarations to omit handler, enabling purely runtime-driven listeners.
  • Updates tests, type tests, docs, and the a11y devframe to use emit() and validate the new runtime subscription behavior.
File summaries
File Description
tests/snapshots/tsnapi/devframe/in-page-channel.snapshot.d.ts Updates public type snapshots to include emit()/on() and relaxed event handler requirements.
plugins/a11y/app/lib/channel.ts Migrates a11y panel-side event sending from callEvent() to emit().
packages/devframe/src/in-page-channel/types.ts Updates public types/docs for protocol semantics; enables event declarations without handlers; adds emit()/on() to endpoint interfaces.
packages/devframe/src/in-page-channel/types.test-d.ts Adds type-level tests for event-without-handler and runtime on() typing.
packages/devframe/src/in-page-channel/panel.ts Adds emit() and on() to the panel endpoint implementation; makes functions required at runtime.
packages/devframe/src/in-page-channel/page-script.ts Adds emit() and on() to the page-script endpoint implementation; deprecates callEvent() to alias emit().
packages/devframe/src/in-page-channel/internal.ts Extends the local registry to support runtime listeners via on() and resolves handlers accordingly.
packages/devframe/src/in-page-channel/index.ts Adjusts defineChannelFunction generics (ARGS default).
packages/devframe/src/in-page-channel/in-page-channel.test.ts Updates runtime tests to validate emit() + runtime subscription/unsubscribe behavior.
docs/content/8.references/5.browser-api.md Adds an in-page channel endpoint API reference table.
docs/content/1.guide/12.in-page-channel.md Updates guide examples and wording to use emit() + runtime on() subscriptions.
Review details

Suppressed comments (1)

packages/devframe/src/in-page-channel/index.ts:40

  • defineChannelFunction() leaves TYPE without a default, so calls that omit type can infer TYPE as the full InPageFunctionType union. With the new conditional handler optionality for events, that can accidentally make handler optional (e.g. defineChannelFunction({ name: 'x' }) becomes type-valid), weakening the helper’s compile-time safety.
export function defineChannelFunction<
  NAME extends string,
  TYPE extends InPageFunctionType,
  ARGS extends any[] = [],
  RETURN = void,
  const AS extends RpcArgsSchema | undefined = undefined,
  const RS extends RpcReturnSchema | undefined = undefined,
>(
  • Files reviewed: 10/11 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docs/content/8.references/5.browser-api.md
Comment thread docs/content/1.guide/12.in-page-channel.md Outdated
Copilot AI review requested due to automatic review settings September 4, 2026 15:36
NAME extends string,
TYPE extends InPageFunctionType,
ARGS extends any[],
ARGS extends any[] = [],

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needed because handler is optional if type is event

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new runtime listener registry currently allows subscribing to undeclared method names and can silently treat missing non-event handlers as no-ops, which risks bypassing intended validation and masking misconfiguration.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

packages/devframe/src/in-page-channel/page-script.ts:67

  • options.functions is assumed present at runtime, but if a non-TypeScript consumer passes {} / omits it, Object.entries(options.functions) will throw a low-signal error. A small runtime assertion would make misconfiguration easier to diagnose.
    packages/devframe/src/in-page-channel/panel.ts:66
  • options.functions is now required by the types, but at runtime (JS usage) Object.entries(options.functions) will throw a generic Cannot convert undefined or null to object if it’s missing. Adding an explicit runtime assertion here would produce a clearer error for consumers who aren’t typechecked.
  • Files reviewed: 10/11 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment on lines +178 to +185
on(name, listener) {
let registered = listeners.get(name)
if (!registered) {
registered = new Set()
listeners.set(name, registered)
}
registered.add(listener)
return () => {
Comment on lines +200 to +204
if (definition?.args?.length)
await validateArgs(definition.name, definition.args, args)
const result = await definition.handler(...args)
if (definition.jsonSerializable)
const result = await definition?.handler?.(...args)
for (const listener of [...(listeners.get(name) ?? [])])
listener(...args)
...args: FnArgs<PanelFunctions<P>[K]>
) => void
/** Subscribe to an event emitted by a panel. Returns an unsubscribe function. */
on: <K extends keyof PageScriptFunctions<P> & string>(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is also wrong: it's probably allowing too many, it should only allow those of type: 'event'

@posva
posva marked this pull request as draft September 4, 2026 15:54
@posva

posva commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

I'm realizing some stuff is still not good enough, so marking as draft

navigation:
icon: i-lucide-globe
description: 'Lookup tables for the browser side: connectDevframe options, RPC client events, connection statuses, and in-page channel error codes.'
description: 'Lookup tables for the browser side: connectDevframe options, RPC client events, connection statuses, and in-page channels.'

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so many useless edits on this one... I'm reverting them

@posva
posva marked this pull request as ready for review September 7, 2026 09:46
Copilot AI review requested due to automatic review settings September 7, 2026 09:46
@vercel

vercel Bot commented Sep 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
devframe Ready Ready Preview Sep 7, 2026 9:50am UTC

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There are a few concrete issues to address (docs wording/example correctness and a runtime Object.entries(options.functions) crash hazard) before it’s safe to approve.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

packages/devframe/src/in-page-channel/types.ts:48

  • Same issue as above: panel “events” are inferred from void return types, which will also treat any () => void request/response action as an event. If you plan to keep inference for now, consider at least documenting this limitation prominently in the in-page channel types/docs.
/**
 * Panel functions whose resolved return type marks an event.
 * @internal
 */
type PanelFunctionsEvents<P extends InPageChannelProtocol> = {
  [K in keyof PanelFunctions<P> as FnReturn<PanelFunctions<P>[K]> extends void ? K : never]: PanelFunctions<P>[K]
}
  • Files reviewed: 12/13 changed files
  • Comments generated: 5
  • Review effort level: Lite

Comment on lines 65 to 67
const registry = createLocalFunctionRegistry(codec)
for (const [fnName, definition] of Object.entries(options.functions ?? {}))
for (const [fnName, definition] of Object.entries(options.functions))
registry.register({ ...definition, name: fnName })
Comment on lines +65 to 66
for (const [fnName, definition] of Object.entries(options.functions))
registry.register({ ...definition, name: fnName })
Comment on lines +34 to +40
/**
* Page-script functions whose resolved return type marks an event.
* @internal
*/
type PageScriptFunctionsEvents<P extends InPageChannelProtocol> = {
[K in keyof PageScriptFunctions<P> as FnReturn<PageScriptFunctions<P>[K]> extends void ? K : never]: PageScriptFunctions<P>[K]
}
```

`callEvent` on the page script is 1:N: it fans out to every connected panel, and panels that don't implement the function ignore it. Request/response *to* a panel goes through an explicit peer handle: `channel.panels[0].call('flash', '…')`.
`emit` on the page-script endpoint is 1:N: it fans out to every connected panel endpoint. Request/response *to* a panel goes through an explicit peer handle: `pageChannel.panels[0].call('flash', '…')`.
navigation:
icon: i-lucide-globe
description: 'Lookup tables for the browser side: connectDevframe options, RPC client events, connection statuses, and in-page channel error codes.'
description: 'Lookup tables for the browser side: connectDevframe options, RPC client events, connection statuses, and in-page channels error codes.'
@antfu
antfu merged commit 120a5a5 into devframes:main Sep 8, 2026
14 checks passed
@posva
posva deleted the feat/in-page-channel-events branch September 8, 2026 07:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants